Skip to contentMethod: processDirectory(File, String)
1: /**
2: * Copyright (C) 2022 Czech Technical University in Prague
3: *
4: * This program is free software: you can redistribute it and/or modify it under
5: * the terms of the GNU General Public License as published by the Free Software
6: * Foundation, either version 3 of the License, or (at your option) any
7: * later version.
8: *
9: * This program is distributed in the hope that it will be useful, but WITHOUT
10: * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11: * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12: * details. You should have received a copy of the GNU General Public License
13: * along with this program. If not, see <http://www.gnu.org/licenses/>.
14: */
15: package cz.cvut.kbss.jsonld.deserialization.util;
16:
17: import cz.cvut.kbss.jsonld.exception.JsonLdException;
18: import org.slf4j.Logger;
19: import org.slf4j.LoggerFactory;
20:
21: import java.io.File;
22: import java.io.IOException;
23: import java.net.MalformedURLException;
24: import java.net.URI;
25: import java.net.URISyntaxException;
26: import java.net.URL;
27: import java.util.Enumeration;
28: import java.util.Objects;
29: import java.util.function.Consumer;
30: import java.util.jar.JarEntry;
31: import java.util.jar.JarFile;
32:
33: /**
34: * Processes classpath accessible to the application and passes all discovered classes to the registered listener.
35: */
36: public class ClasspathScanner {
37:
38: private static final Logger LOG = LoggerFactory.getLogger(ClasspathScanner.class);
39:
40: private static final String JAR_FILE_SUFFIX = ".jar";
41: private static final String CLASS_FILE_SUFFIX = ".class";
42:
43: private final Consumer<Class<?>> listener;
44:
45: public ClasspathScanner(Consumer<Class<?>> listener) {
46: this.listener = Objects.requireNonNull(listener);
47: }
48:
49: /**
50: * Scans classpath accessible from the current thread's class loader.
51: * <p>
52: * All available classes are passed to the registered consumer.
53: * <p>
54: * The {@code scanPath} parameter means that only the specified package (and it subpackages) should be searched.
55: * This parameter is optional, but it is highly recommended to specify it, as it can speed up the process
56: * dramatically.
57: * <p>
58: * Inspired by https://github.com/ddopson/java-class-enumerator
59: *
60: * @param scanPath Package narrowing down the scan space. Optional
61: */
62: public void processClasses(String scanPath) {
63: if (scanPath == null) {
64: scanPath = "";
65: }
66: final ClassLoader loader = Thread.currentThread().getContextClassLoader();
67: try {
68: Enumeration<URL> urls = loader.getResources(scanPath.replace('.', '/'));
69: while (urls.hasMoreElements()) {
70: final URL url = urls.nextElement();
71: if (isJar(url.toString())) {
72: processJarFile(url, scanPath);
73: } else {
74: processDirectory(new File(getUrlAsUri(url).getPath()), scanPath);
75: }
76: }
77: // Scan jar files on classpath
78: Enumeration<URL> resources = loader.getResources(".");
79: while (resources.hasMoreElements()) {
80: URL resourceURL = resources.nextElement();
81: if (isJar(resourceURL.toString()))
82: processJarFile(resourceURL, scanPath);
83: }
84: } catch (IOException e) {
85: throw new JsonLdException("Unable to scan packages.", e);
86: }
87: }
88:
89: private static boolean isJar(String filePath) {
90: return filePath.startsWith("jar:") || filePath.endsWith(JAR_FILE_SUFFIX);
91: }
92:
93: private static URI getUrlAsUri(URL url) {
94: try {
95: // Transformation to URI handles encoding, e.g. of whitespaces in the path
96: return url.toURI();
97: } catch (URISyntaxException ex) {
98: throw new JsonLdException("Unable to scan resource " + url + ". It is not a valid URI.", ex);
99: }
100: }
101:
102: protected void processJarFile(URL jarResource, String packageName) {
103: final String relPath = packageName.replace('.', '/');
104: final String jarPath = jarResource.getPath().replaceFirst("[.]jar[!].*", JAR_FILE_SUFFIX)
105: .replaceFirst("file:", "");
106:
107: LOG.trace("Scanning jar file {} for classes.", jarPath);
108: try (final JarFile jarFile = new JarFile(jarPath)) {
109: final Enumeration<JarEntry> entries = jarFile.entries();
110: while (entries.hasMoreElements()) {
111: final JarEntry entry = entries.nextElement();
112: final String entryName = entry.getName();
113: String className = null;
114: if (shouldSkipEntry(entryName)) {
115: continue;
116: }
117: if (entryName.endsWith(CLASS_FILE_SUFFIX) && entryName.contains(relPath)) {
118: // Remove prefix from multi-release JAR class names
119: className = entryName.replaceFirst("META-INF/versions/[1-9][0-9]*/", "");
120: className = className.replaceFirst("WEB-INF/classes/", "");
121: className = className.replaceFirst("BOOT-INF/classes/", "");
122: className = className.replace('/', '.').replace('\\', '.');
123: className = className.substring(0, className.length() - CLASS_FILE_SUFFIX.length());
124: }
125: if (className != null) {
126: processClass(className);
127: }
128: }
129: } catch (IOException e) {
130: LOG.error("Unable to scan classes in JAR file " + jarPath, e);
131: }
132: }
133:
134: private static boolean shouldSkipEntry(String entryName) {
135: // Skip module-info.class files
136: return entryName.endsWith("module-info" + CLASS_FILE_SUFFIX);
137: }
138:
139: private void processClass(String className) {
140: try {
141: final Class<?> cls = Class.forName(className);
142: listener.accept(cls);
143: } catch (ClassNotFoundException | NoClassDefFoundError e) {
144: LOG.error("Unable to process class " + className, e);
145: }
146: }
147:
148: private void processDirectory(File dir, String packageName)
149: throws MalformedURLException {
150: LOG.trace("Scanning directory {}.", dir);
151: // Get the list of the files contained in the package
152: final String[] files = dir.list();
153:• if (files == null) {
154: return;
155: }
156:• for (String fileName : files) {
157: String className = null;
158: // we are only interested in .class files
159:• if (fileName.endsWith(CLASS_FILE_SUFFIX)) {
160: // removes the .class extension
161: className = packageName + '.' + fileName.substring(0, fileName.length() - 6);
162: }
163:• if (className != null) {
164: processClass(className);
165: }
166: final File subDir = new File(dir, fileName);
167:• if (subDir.isDirectory()) {
168:• processDirectory(subDir, packageName + (!packageName.isEmpty() ? '.' : "") + fileName);
169:• } else if (isJar(subDir.getAbsolutePath())) {
170: processJarFile(subDir.toURI().toURL(), packageName);
171: }
172: }
173: }
174: }